Skip to content

feat(http): add stateless flag to mount_http()#290

Open
easydaniel wants to merge 1 commit intotadata-org:mainfrom
easydaniel:feat/mount-http-stateless
Open

feat(http): add stateless flag to mount_http()#290
easydaniel wants to merge 1 commit intotadata-org:mainfrom
easydaniel:feat/mount-http-stateless

Conversation

@easydaniel
Copy link
Copy Markdown

Summary

Add an opt-in stateless: bool = False parameter to FastApiMCP.mount_http(), threaded through FastApiHttpSessionManager to the underlying StreamableHTTPSessionManager. Default behavior is unchanged.

Motivation

mount_http() currently hardcodes StreamableHTTPSessionManager(stateless=False) (fastapi_mcp/transport/http.py:56). Stateful sessions live in one process's memory dict keyed on Mcp-Session-Id, which makes them unusable in multi-replica deployments behind a load balancer:

  1. Client sends initialize → lands on replica A → A creates a session, returns Mcp-Session-Id: abc.
  2. Client sends tools/list with that session header → lands on replica B.
  3. Replica B has no record of session abc → returns 400 Bad Request: Missing session ID (or 404 Session not found).

Workarounds like sticky-session routing on Mcp-Session-Id at the load balancer layer are fragile and not something we'd like to require from every deployment. Worse, because MCP clients rarely send the DELETE that evicts a session, _server_instances grows unbounded — each initialize leaks one transport until the process restarts.

The MCP spec explicitly permits stateless streamable HTTP (see the stateless docstring on StreamableHTTPSessionManager), and it's the right mode for request/response-only MCP servers: a FastAPI app exposing REST handlers as tools doesn't need progress notifications, resumability, or server-initiated pushes.

Changes

  • fastapi_mcp/server.py — add a stateless: bool = False kwarg to FastApiMCP.mount_http() with a docstring explaining the trade-off; pass it through to FastApiHttpSessionManager.
  • fastapi_mcp/transport/http.pyFastApiHttpSessionManager.__init__ gains stateless: bool = False, stored as self.stateless. The hardcoded stateless=False in _ensure_session_manager_started becomes stateless=self.stateless, and event_store is forced to None in stateless mode (the SDK doesn't track events without sessions). Updated the inline comment to reflect that stateful/stateless is now a user choice.
  • tests/test_http_real_transport_stateless.py (new) — mirrors the existing test_http_real_transport.py real-uvicorn fixture pattern, mounts with stateless=True, and asserts:
    • test_stateless_initialize_has_no_session_header — initialize must NOT emit mcp-session-id.
    • test_stateless_tools_list_without_sessiontools/list succeeds without prior initialize and without any session header.
    • test_stateless_call_tool_without_sessiontools/call succeeds without prior initialize and without any session header.

Test Plan

  • ruff check . passes.
  • mypy fastapi_mcp/transport/http.py fastapi_mcp/server.py — no issues in the touched files (pre-existing error in fastapi_mcp/types.py:135 is unrelated — pydantic API drift).
  • pytest tests/test_http_real_transport_stateless.py tests/test_http_real_transport.py — new stateless tests pass and existing stateful tests still pass, confirming backwards compatibility.
  • Default behavior (no stateless arg) unchanged — existing test suite untouched.
  • CI (py 3.10 / 3.11 / 3.12 matrix).

Backwards Compatibility

None broken. stateless defaults to False, which preserves the 0.4.0 behavior exactly. Existing users opt in by passing mount_http(stateless=True).

Use Case

We're running a stateless REST-gateway style MCP server (FastApiMCP over our external API gateway) as a multi-replica deployment behind Kong. Every tool is a 1:1 wrapper over a FastAPI handler with a single JSON-RPC request/response — no progress, no streams, no resumability. Without this flag we have to reimplement mount_http in our own repo to call StreamableHTTPSessionManager directly; with it we get a one-line upgrade back to the supported public API.

Happy to adjust the docstring wording, test layout, or anything else — thanks for the library!

mount_http() currently hardcodes StreamableHTTPSessionManager(stateless=False).
Stateful sessions are held in one process's memory, which makes them
unusable in multi-replica deployments behind a load balancer: without
sticky-session routing on the Mcp-Session-Id header, a client's follow-up
request lands on a different replica and fails with "Session not found".

The MCP spec allows stateless streamable HTTP (see
StreamableHTTPSessionManager's `stateless` param in the python-sdk), and
it's the right mode for request/response-only MCP servers — a FastAPI app
exposing REST handlers as tools doesn't need progress notifications,
resumability, or server-initiated pushes.

Add an opt-in `stateless: bool = False` parameter to `mount_http()`,
threaded through `FastApiHttpSessionManager` to the underlying
`StreamableHTTPSessionManager`. The default is unchanged, so existing
users are unaffected.

Tests:
- tests/test_http_real_transport_stateless.py mirrors the existing
  real-transport tests with `mount_http(stateless=True)` and asserts:
  - initialize response does NOT return an mcp-session-id header
  - tools/list succeeds without a prior initialize or session header
  - tools/call succeeds without a prior initialize or session header

This unblocks stateless REST-gateway style MCP servers running multiple
replicas.
@easydaniel easydaniel closed this Apr 9, 2026
@easydaniel easydaniel reopened this Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant